--﻿ DungeonShopM
-- Create by chengb Jan/8/2015
-- 地牢商店模块

module("DungeonShopM", package.seeall);

-- 当前出售的商品信息
local curSellGoods = {};

-- 配置表中所有的商品信息
local goodsTable = {};

-- 地牢商店规则
local shopRule = {};

-- 玩家商店信息
local goodsByType = {};

local whenDoBonus;

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    goodsTable = {};
    shopRule = {};
    goodsByType = {};

    -- 转换下dungeon_shop_goods_CSV配置表
    local dungeon_shop_goods_CSV = get_dungeon_shop_goods_CSV();
    for i = 1, #dungeon_shop_goods_CSV do
        -- 解析出商品信息
        local id = dungeon_shop_goods_CSV[i].id;
        local classify = dungeon_shop_goods_CSV[i].classify;

        goodsTable[id] = parseGoods(dungeon_shop_goods_CSV[i]);

        -- 归类
        goodsByType[classify] = goodsByType[classify] or {};
        table.insert(goodsByType[classify], goodsTable[id]);
    end
    -- TODO: 这里赋值nil不起效，暂不知为何
    dungeon_shop_goods_CSV = {};

    -- 对每一类都排序，为了和服务器随机时一致
    for type, _ in pairs(goodsByType) do
        -- 每一类里面都按id升序排序
        table.sort(goodsByType[type], function(a, b) return a.id < b.id; end);
    end

    -- 转换dungeon_shop_rule_CSV配置表
    local dungeon_shop_rule_CSV = get_dungeon_shop_rule_CSV();
    for i = 1, #dungeon_shop_rule_CSV do
        local classify = dungeon_shop_rule_CSV[i].classify;
        shopRule[classify] = dungeon_shop_rule_CSV[i];
    end
    dungeon_shop_rule_CSV = {};
end

-- 模块初始化
function init()
    loadCsv();

    -- 关注玩家离开地牢的事件
    EventMgr.removeAll("DungeonShopM");
    EventMgr.register("DungeonShopM", event.LEAVE_DUNGEON, function()
        ME.user.dbase:delete("goods_stat");
        ME.user.dbase:delete("goods_buy_stat");
    end);

    -- 关注玩家进入下一层的事件
    EventMgr.register("DungeonShopM", event.NEXT_FLOOR, function()
        curSellGoods = {};
    end);

    -- 关注奖励事件
    EventMgr.register("DungeonShopM", event.DO_BONUS, whenDoBonus);
end

-- 奖励回调
whenDoBonus = function(para)
    -- 如果不是在迷宫内的
    if not DungeonM.isInDungeon() or DUNGEON_GAMING ~= DungeonM.getState() then
        return;
    end

    -- 如果不是大地之门奖励
    if para[1] ~= 1 or para[2] ~= 4005 then
        return;
    end

    -- 称号奖励的，相关字段没被限制
    if ME.user.dbase:query("has_back_floor", 0) < 1 then
        return;
    end

    -- 找到卷轴商店
    local shopGrid;
    local grids = DungeonM.getCurrentDungeon();
    for _, grid in pairs(grids) do
        if grid.type == GRID_TYPE_SHOP
            and grid.shopType == "spell" then
            shopGrid = grid;
            break;
        end
    end

    if not shopGrid then
        -- 没有卷轴商店
        return;
    end

    local list = getSellGoodsByType(shopGrid.shopType .. shopGrid.pos);
    for _, info in pairs(list) do
        local goodsId    = info.goodsId;
        local soldOut    = iif(info["soldOut"], info["soldOut"], false);
        if not soldOut and goodsId == 5 then
            -- 换个商品
            local rand1 =  DungeonM.getRandSeed("shop_has_back_floor");
            -- 转为同阶位卷轴
            local arr = { 10, 15, 20, 25, 30, };
            info.goodsId = arr[1 + rand1 % 5];
            break;
        end
    end
end

-- 解析商品数据
function parseGoods(info)
    -- 商品
    local arr = string.explode(string.sub(info.goods, 3, -2), ",")
    info.goods = { 1, tonumber(arr[1]), tonumber(arr[2]), }

    -- 价格
    arr = string.explode(string.sub(info.price, 3, -2),",");
    if string.startWith(info.price, "1(") then
        info.price = { 1, tonumber(arr[1]), tonumber(arr[2]), };
    else
        info.price = { 2, arr[1], tonumber(arr[2]), };
    end

    return info;
end

-- 检索
function query(goodsId, path)
    local m = goodsTable[goodsId];

    if not m or not path then
        return m;
    end

    return m[path];
end

-- 获取商品
function queryGoods(goodsId)
    -- 返回商品
    return goodsTable[goodsId].goods;
end

-- 检索商品价格
function queryPrice(goodsId)
    local arr   = goodsTable[goodsId].price;
    local cost;
    local prop;

    -- 地牢中价格固定
    cost = arr[3];

    if (goodsTable[goodsId].classify == "spell" or
        goodsTable[goodsId].classify == "treasure") then
        -- 商人冈布奥技能对卷轴、宝物商店有折扣
        local rate  = 1000;
        local props = PropM.fetchProps(ME.user, "shop");
        for _, prop in ipairs(props) do
            rate = rate - PropM.apply(prop, 0);
        end
        if rate < 0 then
            rate = 0
        end
        cost = math.modf(cost * rate / 1000);
    end

    -- 降低探索点消耗的属性
    if goodsTable[goodsId].price[2] == "search_point" then
        prop = PropM.combine(ME.user, "cost", "search_point");
        cost = PropM.apply(prop, cost);
    end

    -- 海贼酒馆消耗降低
    if goodsTable[goodsId].classify == "tavern" then
        prop = PropM.combine(ME.user, "tavern_cost", 1);
        cost = PropM.apply(prop, cost);
    end

    -- 商店消耗降低的属性
    prop = PropM.combine(ME.user, "shop_cost", goodsTable[goodsId].classify);
    cost = PropM.apply(prop, cost);

    return { arr[1], arr[2], cost, };
end

-- 检索商店配置
function queryShopRule(shopType, path)
    if not shopRule[shopType] then
        return nil;
    end

    if path == nil or path == "" then
        return shopRule[shopType];
    end

    return shopRule[shopType][path];
end

-- 抽取地牢商品
function fetchDungeonGoods(shopType, pos)
    local goods = {};
    local allGoods = getGoodsByType(shopType);
    local totalCount = shopRule[shopType]["total_count"];
    local count = totalCount;
    local rand;
    local filterArr = {};

    if shopType == "garden" then
        -- 园丁小屋抽取
        return fetchDungeonGoodsGarden(shopType, pos);
    elseif shopType == "ghost_dock" then
        -- 幽灵船坞抽取
        return fetchDungeonGoodsGhostDock(shopType,pos);
    elseif shopType == "devil" then
        -- 抽取血腥要塞-恶魔集市
        return fetchDungeonGoodsDevil(shopType, pos);
    end

    -- 如果配置了抽取公式，直接使用公式抽取
    local formulaName = queryShopRule(shopType, "fetch_formula");
    if type(formulaName) == "string" and formulaName ~= "" then
        local goodsList = FormulaM.invoke(formulaName, ME.user);

        -- 根据编号升序
        table.sort(goodsList, function(a, b) return a["goodsId"] < b["goodsId"]; end);

        -- 更新商品信息
        updateCurrentSellGoods(shopType .. pos, goodsList);

        -- 需要统计的，统计一遍
        local dbase = shopRule[shopType]["dbase"] or {};
        if dbase["need_stat"] == 1 then
            for _, good in pairs(goodsList) do
                statGoodsTimes(good["goodsId"]);
            end
        end

        return goodsList;
    end

    local props  = PropM.fetchProps(ME.user, "shop");
    if shopType == "spell" or shopType == "treasure" then
        for _, prop in ipairs(props) do
            count = count + tonumber(prop[2]);
        end
    end

    --商品数量加
    local prop = PropM.combine(ME.user, "shop_num", shopType);
    count = count + PropM.apply(prop, 1);

    -- 需要特殊抽取一定一个出现的物品
    local goods_1 = getGoodsByType(shopType.."_1");
    local good = {};
    if type(goods_1) == "table" and #goods_1 > 0 then
        rand = DungeonM.getRandSeed("shop");
        good = { ["goodsId"] = goods_1[1 + rand % #goods_1].id, };

        table.insert(goods, good);

        -- TODO: 如果是不限次数的，则需要计入数量
        -- 策划需求：限定次数的情况下，商品总数允许超过上限
        if (query(good.goodsId, "times") == NUMBER_INFINITY) then
            count = count - 1;
        end
    end

    -- 需要卖药剂配方的
    local goods_2 = getGoodsByType(shopType.."_2");
    local dungeonId = DungeonM.getDungeonId();
    local layer     = DungeonM.currentLayer();
    local maxFloor = DungeonAreaM.query(dungeonId, "max_floor");
    good = {};
    if type(goods_2) == "table" and #goods_2 > 0 and layer ~= maxFloor then
        local formulaId;
        local reagentId;
        local item;
        local items = ME.user:getItems();
        for i = 1, #goods_2 do
            formulaId = goods_2[i].goods[2];
            reagentId = ReagentM.getReagentByFormula(formulaId);
            item  = items[formulaId];
            if goods_2[i].area == dungeonId and item == nil and
               not ReagentM.isReagentActive(reagentId) then
                good = {["goodsId"] = goods_2[i].id};
                -- 每关限1个，且不占商品数量
                table.insert(goods, good);
                break;
            end
        end
    end

    -- 随机抽取count个商品
    local appearBackFloor = 0;
    for i = 1, count do
        -- 获取一个随机数，这个随机数不能随便乱用，一定要确定和服务器严格同步
        rand = DungeonM.getRandSeed("shop");

        -- 抽取到一个商品
        good = fetchByRand(allGoods, rand, filterArr);

        -- 大地之门最多1本且每次最多出现1本
        if good[1]["goodsId"] == 5 then
            if ME.user.dbase:query("has_back_floor", 0) >= 1 or
               appearBackFloor == 1 then
                local rand1 =  DungeonM.getRandSeed("shop_has_back_floor");
                -- 转为同阶位卷轴
                local arr = { 10, 15, 20, 25, 30, };
                good[1]["goodsId"] = arr[1 + rand1 % 5];
            end

            -- 记录大地之门已出现
            appearBackFloor = 1;
        end

        goods = table.append(goods, good);

        for _, data in pairs(good) do
            if query(data.goodsId, "unrepeat") == 1 then
                -- 避免抽取一样的
                table.insert(filterArr, data.goodsId);
            end
        end
    end

    -- 根据编号升序
    table.sort(goods, function(a, b) return a["goodsId"] < b["goodsId"]; end);

    -- 更新商品信息
    updateCurrentSellGoods(shopType .. pos, goods);

    -- 是否要统计
    for _, good in pairs(goods) do
        statGoodsTimes(good["goodsId"]);
    end

    return goods;
end

-- 恶魔集市
function fetchDungeonGoodsDevil(shopType, pos)
    local goods = {};
    local allGoods = getGoodsByType(shopType);
    local totalCount = shopRule[shopType]["total_count"];
    local count = totalCount;
    local rand;
    local filterArr = {};

    local props  = PropM.fetchProps(ME.user, "shop");
    if shopType == "spell" or shopType == "treasure" then
        for _, prop in ipairs(props) do
            count = count + tonumber(prop[2]);
        end
    end

    -- 抽取套装
    -- 其中2件为血腥要塞怪物掉落的套装：
    -- 两件装备必定不同星阶
    -- 两件装备必定不同套
    local goods_1 = getGoodsByType(shopType.."_suit");
    local good = {};
    if type(goods_1) == "table" and #goods_1 > 0 then
        rand = DungeonM.getRandSeed("shop");
        local index = rand % #goods_1 + 1;

        -- 先随机抽取一个
        local suit1 = index;
        good = { ["goodsId"] = goods_1[index].id, };

        table.insert(goods, good);

        -- 消耗一个数量
        count = count - 1;

        -- 另外一个
        local backupList = {};
        for index = 1, #goods_1 do
            repeat
                if index == suit1 then
                    break;
                end

                -- 如果阶位一样
                local id1 = queryGoods(goods_1[suit1].id)[2];
                local id2 = queryGoods(goods_1[index].id)[2];

                if EquipM.query(id1, "rank") == EquipM.query(id2, "rank") then
                    break;
                end

                -- 如果是同一套
                if EquipM.query(id1, "suit") == EquipM.query(id2, "suit") then
                    break;
                end

                table.insert(backupList, index);
            until true;
        end

        if #backupList > 0 then
            local rand = DungeonM.getRandSeed("shop");
            local index = rand % #backupList + 1;

            good = { ["goodsId"] = goods_1[backupList[index]].id, };

            table.insert(goods, good);

            count = count - 1;
        end
    end

    -- 需要卖药剂配方的
    local goods_2 = getGoodsByType(shopType.."_2");
    local dungeonId = DungeonM.getDungeonId();
    local layer     = DungeonM.currentLayer();
    local maxFloor = DungeonAreaM.query(dungeonId, "max_floor");
    good = {};
    if type(goods_2) == "table" and #goods_2 > 0 and layer ~= maxFloor then
        local formulaId;
        local reagentId;
        local item;
        local items = ME.user:getItems();
        for i = 1, #goods_2 do
            formulaId = goods_2[i].goods[2];
            reagentId = ReagentM.getReagentByFormula(formulaId);
            item  = items[formulaId];
            if goods_2[i].area == dungeonId and item == nil and
               not ReagentM.isReagentActive(reagentId) then
                good = {["goodsId"] = goods_2[i].id};
                -- 每关限1个，且不占商品数量
                table.insert(goods, good);
                break;
            end
        end
    end

    -- 先把非法的过滤掉
    local list = {};
    for index = 1, #allGoods do
        repeat
            local data = allGoods[index];
            local goodsId = data["id"];
            local goodsInfo = goodsTable[goodsId];

            if goodsInfo["times"] ~= NUMBER_INFINITY and
                goodsInfo["times"] <= DungeonServiceM.getItemStat(goodsInfo["goods"][2]) then
                break;
            end

            table.insert(list, data);

        until true;
    end

    allGoods = list;

    -- 随机抽取count个商品
    for i = 1, count do
        -- 获取一个随机数，这个随机数不能随便乱用，一定要确定和服务器严格同步
        rand = DungeonM.getRandSeed("shop");

        -- 抽取到一个商品
        good = fetchByRand(allGoods, rand, filterArr);

        goods = table.append(goods, good);

        for _, data in pairs(good) do
            if query(data.goodsId, "unrepeat") == 1 then
                -- 避免抽取一样的
                table.insert(filterArr, data.goodsId);
            end
        end
    end

    -- 更新商品信息
    updateCurrentSellGoods(shopType .. pos, goods);

    -- 目前恶魔集市都是无限的，暂时把统计关闭
    -- 统计一次
    -- for _, good in pairs(goods) do
    --     statGoodsTimes(good["goodsId"]);
    -- end

    return goods;
end

-- 抽取园丁小屋地牢商品
function fetchDungeonGoodsGarden(shopType, pos)
    local goods = {};
    local allGoods = getGoodsByType(shopType);
    local totalCount = shopRule[shopType]["total_count"];
    local count = totalCount;
    local rand;
    local filterArr = {};

    -- 需要特殊抽取一定一个出现的物品
    local goods_1 = getGoodsByType(shopType.."_1");
    local left_goods_1 = {};

    -- 把已满技能从goods_1中滤除掉
    for i = 1, #goods_1 do
        if not BineM.checkSkillLevelMax(goods_1[i].id) then
            -- 可以学习或者能升级，保留
            table.insert(left_goods_1, goods_1[i]);
        end
    end

    -- 滤除替换
    goods_1 = left_goods_1;

    local good = {};
    if type(goods_1) == "table" and #goods_1 > 0 then
        rand = DungeonM.getRandSeed("shop");
        good = { ["goodsId"] = goods_1[1 + rand % #goods_1].id, };

        if BineM.checkGiveBloodyCuscutae() then
            -- 魔藤技能物品保底
            good = { ["goodsId"] = 204 };

            trace("DungeonShopM", "园丁小屋商店技能保底出现(204)商品");
        end

        table.insert(goods, good);

        -- TODO: 如果是不限次数的，则需要计入数量
        -- 策划需求：限定次数的情况下，商品总数允许超过上限
        if (query(good.goodsId, "times") == NUMBER_INFINITY) then
            count = count - 1;
        end
    else
        -- 都不能升级固定给复活泉和两种子
        goods = { { ["goodsId"] = 203 }, { ["goodsId"] = 201 }, { ["goodsId"] = 202 }};
        count = 0;
    end

    -- 需要卖药剂配方的
    local goods_2 = getGoodsByType(shopType.."_2");
    local dungeonId = DungeonM.getDungeonId();
    local layer     = DungeonM.currentLayer();
    local maxFloor = DungeonAreaM.query(dungeonId, "max_floor");
    good = {};
    if type(goods_2) == "table" and #goods_2 > 0 and layer ~= maxFloor then
        local formulaId;
        local reagentId;
        local item;
        local items = ME.user:getItems();
        for i = 1, #goods_2 do
            formulaId = goods_2[i].goods[2];
            reagentId = ReagentM.getReagentByFormula(formulaId);
            item  = items[formulaId];
            if goods_2[i].area == dungeonId and item == nil and
                not ReagentM.isReagentActive(reagentId) then
                good = {["goodsId"] = goods_2[i].id};
                -- 每关限1个，且不占商品数量
                table.insert(goods, good);
                break;
            end
        end
    end

    -- 随机抽取count个商品
    for i = 1, count do
        -- 获取一个随机数，这个随机数不能随便乱用，一定要确定和服务器严格同步
        rand = DungeonM.getRandSeed("shop");

        -- 抽取到一个商品
        good = fetchByRand(allGoods, rand, filterArr);

        goods = table.append(goods, good);

        for _, data in pairs(good) do
            if query(data.goodsId, "unrepeat") == 1 then
                -- 避免抽取一样的
                table.insert(filterArr, data.goodsId);
            end
        end
    end

    -- 更新商品信息
    updateCurrentSellGoods(shopType .. pos, goods);

    -- 统计一次
    for _, good in pairs(goods) do
        statGoodsTimes(good["goodsId"]);
    end

    return goods;
end

-- 幽灵船坞抽取规则
function fetchDungeonGoodsGhostDock(shopType, pos)
    local goods = {};
    local allGoods = getGoodsByType(shopType);
    local leftAllGoods = {};
    local totalCount = shopRule[shopType]["total_count"];
    local count = totalCount;
    local rand;
    local filterArr = {};

    -- 需要特殊抽取一定一个出现的物品
    local goods_1 = getGoodsByType(shopType.."_1");
    local good = {};
    if type(goods_1) == "table" and #goods_1 > 0 then
        rand = DungeonM.getRandSeed("shop");
        good = { ["goodsId"] = goods_1[1 + rand % #goods_1].id, };

        if good["goodsId"] == 701 then
            -- 幽灵船员需要判断能否升级
            if GhostShipM.checkCanLevelUp() then
                table.insert(goods, good);

                -- TODO: 如果是不限次数的，则需要计入数量
                -- 策划需求：限定次数的情况下，商品总数允许超过上限
                if (query(good.goodsId, "times") == NUMBER_INFINITY) then
                    count = count - 1;
                end
            end
        else
            table.insert(goods, good);

            -- TODO: 如果是不限次数的，则需要计入数量
            -- 策划需求：限定次数的情况下，商品总数允许超过上限
            if (query(good.goodsId, "times") == NUMBER_INFINITY) then
                count = count - 1;
            end
        end
    end

    -- 特殊抽取下一个一定出现的
    local goods_2 = getGoodsByType(shopType.."_2");
    good = {};
    if type(goods_2) == "table" and #goods_2 > 0 then
        rand = DungeonM.getRandSeed("shop");
        good = { ["goodsId"] = goods_2[1 + rand % #goods_1].id, };
        table.insert(goods, good);
        -- TODO: 如果是不限次数的，则需要计入数量
        -- 策划需求：限定次数的情况下，商品总数允许超过上限
        if (query(good.goodsId, "times") == NUMBER_INFINITY) then
            count = count - 1;
        end
    end

    -- 检查幽灵船技能保底
    if GhostShipM.checkGiveDragonCarronade() then
        local skillGood = { ["goodsId"] = 702 };

        rand = DungeonM.getRandSeed("shop");

        table.insert(goods, skillGood);

        trace("DungeonShopM", "幽灵船坞商店技能保底出现(702)商品");

        -- TODO: 如果是不限次数的，则需要计入数量
        -- 策划需求：限定次数的情况下，商品总数允许超过上限
        if (query(good.goodsId, "times") == NUMBER_INFINITY) then
            count = count - 1;
        end
    end

    -- 去除技能满的物品
    for i = 1, #allGoods do
        if not GhostShipM.checkSkillLevelMax(allGoods[i].id) then
            -- 可以学习或者可以升级，保留
            table.insert(leftAllGoods, allGoods[i]);
        end
    end

    -- 滤除替换
    allGoods = leftAllGoods;

    if #allGoods <= 0 then
        count = 0;
    end

    -- 随机抽取count个商品
    for i = 1, count do
        -- 获取一个随机数，这个随机数不能随便乱用，一定要确定和服务器严格同步
        rand = DungeonM.getRandSeed("shop");

        -- 抽取到一个商品
        good = fetchByRand(allGoods, rand, filterArr);

        goods = table.append(goods, good);

        for _, data in pairs(good) do
            if query(data.goodsId, "unrepeat") == 1 then
                -- 避免抽取一样的
                table.insert(filterArr, data.goodsId);
            end
        end
    end

    -- 更新商品信息
    updateCurrentSellGoods(shopType .. pos, goods);

    -- 统计一次
    for _, good in pairs(goods) do
        statGoodsTimes(good["goodsId"]);
    end

    return goods;
end

-- 获取商品出现次数
function getGoodsTimes(goodsId)
    local stat = ME.user.dbase:query("goods_stat", {});

    return stat[goodsId] or 0;
end

-- 统计商品次数
function statGoodsTimes(goodsId)
    local info = query(goodsId);

    if info["times"] == NUMBER_INFINITY then
        -- 没有次数限制的，不需要统计了
        return;
    end

    local context = ME.user.dbase:query("dungeon_context");
    if sizeof(context) > 0 then
        -- 有上下文，不统计
        return;
    end

    -- 增加次数
    local stat = ME.user.dbase:query("goods_stat", {});
    stat[goodsId] = iif(stat[goodsId], stat[goodsId], 0) + 1;

    ME.user.dbase:set("goods_stat", stat);
end

-- 获取商品购买次数
function getGoodsBuyTime(goodsId)
    local stat = ME.user.dbase:query("goods_buy_stat", {});

    return stat[goodsId] or 0;
end

-- 统计商品购买次数
function statGoodsBuyTimes(goodsId)
    local info = query(goodsId);

    if info["buy_stat"] ~= 1 then
        -- 没有次数统计要求的，不需要统计了
        return;
    end

    -- 增加次数
    local stat = ME.user.dbase:query("goods_buy_stat", {});
    stat[goodsId] = iif(stat[goodsId], stat[goodsId], 0) + 1;

    ME.user.dbase:set("goods_buy_stat", stat);
end

-- 抽取某类型的所有商品
function getGoodsByType(type)
    local ret = {};
    local list = goodsByType[type] or {};
    for _, goods in ipairs(list) do
        -- 还没超出次数
        if  goods["times"] == NUMBER_INFINITY or
            goods["times"] > getGoodsTimes(goods["id"]) then
            table.insert(ret, goods);
        end
    end

    return ret;
end

-- 根据随机数抽取商品
function fetchByRand(goods, rand, filterArr)
    local sum = 0;
    local filter = {};

    -- 所有的权和
    for index = 1, #goods do
        repeat
            if table.indexOf(filterArr, goods[index].id) ~= -1 then
                filter[goods[index].id] = true;
                break;
            end

            sum = sum + goods[index].ratio;
        until true;
    end

    -- 将随机数控制在区间内
    rand = rand % sum;
    sum = 0;

    -- 随机数落到哪个商品的区间内就选哪个商品
    local ret = {};
    local goodsId;
    for index = 1, #goods do
        repeat
            if filter[goods[index].id] then
                break;
            end
            sum = sum + goods[index].ratio;

            if sum > rand then
                goodsId = goods[index].id;

                filter[goodsId] = true;

                table.insert(ret, {["goodsId"] = goodsId, });
                break;
            end
        until true;

        if goodsId then
            break;
        end
    end

    if not goodsId or
        #goodsTable[goodsId].group <= 0 then
        return ret;
    end

    -- 把同一组的也加进来
    for index = 1, #goods do
        if  not filter[goods[index].id] and
            #goods[index].group > 0 and
            goods[index].group == goodsTable[goodsId].group then
            table.insert(ret, {["goodsId"] = goods[index].id, });
        end
    end

    return ret;
end

----------------------------------
-- 购买商品
-- @param pos         地牢格子的位置
-- @param shopType    商店类型
-- @param goodsIndex  购买的商品索引
-- @return 购买是否成功
function buyGoods(pos, shopType, goodsIndex)
    if not DungeonM.checkDungeonAction() then
        -- 经检测，不允许执行地牢行为
        return false;
    end

    -- 经判断，不能购买
    if true ~= canBuyGoods(shopType, goodsIndex) then
        return false;
    end

    -- 获取不到该类型商店的商品信息，不处理
    if not curSellGoods or not curSellGoods[shopType] then
        return false;
    end

    local goodsInfo = curSellGoods[shopType][goodsIndex];
    if not goodsInfo then
        return false;
    end

    -- 扣除消耗
    local goodsId = goodsInfo.goodsId;
    local price = queryPrice(goodsId);
    local ok = true;

    if price[1] == 1 then
        -- 物品
        ok = ItemM.costAmount(ME.user, price[2], price[3]);
    elseif price[1] == 2 then
        -- 属性
        ok = ME.user:costAttrib(price[2], price[3]);
    else
        return false;
    end

    if not ok then
        return false;
    end

    -- 增加同步操作缓存
    -- 必须在doBonus之前，因为doBonus也会触发新的action
    DungeonM.addAction({ ["cmd"] = "buy_goods", ["pos"] = pos, ["data"] = goodsIndex });

    if query(goodsId, "repeat_buy") ~= 1 then
        -- 如果该商品不能重复购买，标记为已售罄
        soldOutGoods(shopType, goodsIndex);
    end

    -- 获得物品
    local item = table.deepcopy(queryGoods(goodsId));
    local result = BonusM.doBonus(item, "shop");

    -- 记录购买次数
    statGoodsBuyTimes(goodsId);

    -- 购买属性奖励
    bonusDungeonAttrib(pos);

    -- 抛出购买地牢商品的事件
    EventMgr.fire(event.BUY_DUNGEON_GOODS, {
        ["pos"]         = pos,
        ["shopType"]    = shopType,
        ["goodsIndex"]  = goodsIndex,
        ["goodsId"]     = goodsId,
        ["classId"]     = iif(result, result[2], nil),
    });

    -- action已经添加了
    return true, true;
end

-- 购买商品后的属性奖励
function bonusDungeonAttrib(pos)
    local grid = DungeonM.getGridByPos(pos);
    if not grid then
        return;
    end

    local shopType = grid.shopType;
    local dbase = queryShopRule(shopType, "dbase") or {};
    local bonus = dbase["add_attrib"];
    if type(bonus) ~= "table" then
        return;
    end

    DungeonServiceM.addDungeonAttrib(bonus[1], bonus[2]);
end

----------------------
-- 判断能否购买物品
-- @param shopType    商店类型
-- @param goodsIndex  商品索引
function canBuyGoods(shopType, goodsIndex)
    -- 获取不到该类型商店的商品信息，不处理
    if not curSellGoods or not curSellGoods[shopType] then
        cclog("获取不到地牢商店(%s)的商品信息。", shopType);
        return false;
    end

    local goodsInfo = curSellGoods[shopType][goodsIndex];
    if not goodsInfo then
        cclog("商品序号(%d)不存在。", goodsIndex);
        return false;
    end

    -- 如果已经买过了
    if goodsInfo.soldOut then
        return "sold_out";
    end

    local price = queryPrice(goodsInfo.goodsId);
    local ok = true;

    if price[1] == 1 then
        -- 物品
        ok = ItemM.getAmount(ME.user, price[2]) >= price[3]
        if not ok then
            return string.format("item_%d_lack", price[2]);  -- 物品数量不足
        end
    elseif price[1] == 2 then
        -- 属性
        ok = ME.user.dbase:query(price[2], 0) >= price[3]
        if not ok then
            return price[2] .. "_lack";  -- 属性数量不足
        end
    else
        return false;
    end

    return ok;
end

--------------------------
-- 更新当前出售的商品信息
-- @param shopType  商店类型（如宝物商店、卷轴商店）
-- @param goods     该店出售的商品信息
function updateCurrentSellGoods(shopType, goods)
    curSellGoods[shopType] = goods;
end

---------------------------
-- 获取某类型商店的商品信息
-- @param shopType 商店类型
function getSellGoodsByType(shopType)
    return curSellGoods[shopType];
end

-- 获取商品列表
function getSellGoods()
    return curSellGoods;
end

---------------------------
-- 将商品标记为已售罄
-- @param shopType     商店类型
-- @param goodsIndex   商品索引
function soldOutGoods(shopType, goodsIndex)
    -- 获取不到该类型商店的商品信息，不处理
    if not curSellGoods or not curSellGoods[shopType] then
        return;
    end

    local goodsInfo = curSellGoods[shopType][goodsIndex];
    if not goodsInfo then
        -- 指定商品不存在
        return;
    end

    goodsInfo.soldOut = true;
end

-- 黑市摊贩偷窃物品
function tryStealBlackMarketShop(pos)
    local grid = DungeonM.getGridByPos(pos);

    -- 如果不是黑市摊贩
    if grid.type ~= GRID_TYPE_BLACK_MARKET or
        grid.state == GRID_STATE_DISABLE then
        trace("ChessTableM", "不是黑市摊贩或已失效。");
        return false;
    end

    -- 结果
    local result;
    local bonusList;

    -- 设置失效
    grid:changeState(GRID_STATE_DISABLE);

    local rand1 = DungeonM.getRandSeed("black_market_steal") % 100;

    if ME.user.dbase:query("active_pet") == 33 then
        -- 怪盗必定成功
        rand1 = 0;
    end

    if rand1 < 50 then
        -- 成功
        local bonus;
        bonusList = FormulaM.invoke("CALC_BLACK_MARKET_STEAL_BONUS");
        for _, bonus in pairs(bonusList) do
            -- 奖励
            BonusM.doBonus(bonus, "black_market_steal");
        end
        result = "success";
    else
        -- 召唤两个怪物
        local elements = grid.monster_elements;
        for i = 1, 2 do
            local index = DungeonM.getRandSeed("black_market_steal") % #elements;
            local targetGrid = DungeonM.findDisabledGrid();
            if targetGrid then
                local targetPos = targetGrid:getPos();
                local element = elements[index + 1]; -- 客户端下标+1
                DungeonM.transGridToMonster(targetPos, element);
            end
        end
        result = "fail";
    end

    -- 抛出事件
    EventMgr.fire(event.STEAL_BLACK_MARKET, {["pos"] = pos, ["bonus"] = bonusList, ["result"] = result});

    return true;
end

-- 黑市摊贩交易
function tryDealBlackMarketShop(pos)
    local grid = DungeonM.getGridByPos(pos);

    -- 如果不是黑市摊贩
    if grid.type ~= GRID_TYPE_BLACK_MARKET or
        grid.state == GRID_STATE_DISABLE then
        trace("ChessTableM", "不是黑市摊贩或已失效。");
        return false;
    end

    -- 设置失效
    grid:changeState(GRID_STATE_DISABLE);

    local element = grid.shop_element;
    local class = DungeonM.queryElement(element, "class");

    -- 直接转成商店
    DungeonM.transformGrid(class, pos, GRID_STATE_OPEN, {["element"] = element});

    return true;
end
